package ngat.oss.simulation;

import java.util.*;

import ngat.phase2.*;
import ngat.util.*;
import ngat.util.logging.*;

/** A look-ahead scheduler which uses a time-quantized series of candidate lists at fixed intervals
 * in the future. The scheduler assumes the environment will remain stable for a fixed horizon,
 * the quantum interval is some fixed fraction of this horizon. A series of potential schedule sequences
 * is calculated and the top ranking scheme is selected and items are picked off this by the despatcher
 * until either the environment changes (break) or the sequence is exhausted.
 *
 * Setup parameters:
 *
 * <dl> 
 *   <dt>horizon 
 *      <dd>Length of horizon over which env is expected to remain stable.
 *   <dt>dtq 
 *      <dd>Length of quantization step - candidate lists are calculated at this interval.
 *   <dt>nSeq 
 *      <dd>Number of sequences to test (alternative is to specify a convergence time or imporvement threshold).
 * </dl>
 */
public class QuantumLookaheadScheduler implements Despatcher {

    /** Length of horizon over which env is expected to remain stable.*/
    private long horizon;

    /** Length of quantization step - candidate lists are calculated at this interval.*/
    private long dtq;

    /** Number of sequences to test.*/
    private int nSeq;

    /** End of current look-ahead horizon.*/
    private long currHorizonEnd;

    /** Environment prediction for current look-ahead horizon.*/
    private EnvironmentSnapshot currEnv;

    /** Sequence for current look-ahead horizon.*/
    private List currSequence;

    /** Candidate generator.*/
    private CandidateGenerator    candGen;

    /** Execution history.,*/
    private ExecutionHistoryModel  historyModel;

    private EnhancedExecutionHistoryModel  eHistoryModel;

    /** Accounting information.*/
    private AccountingModel accModel;

    private EnhancedAccountingModel eAccModel;

    /** Execution timing and feasibility model.*/
    private ExecutionTimingModel execModel;

    /** List of candidate lists at future times.*/
    private List timedLists;  

    /** The NOW time.*/
    private long timeNow;

    /** Logger.*/
    private LogProxy logger;

    int iseq = 0;

    public QuantumLookaheadScheduler(CandidateGenerator    candGen,
				     ExecutionHistoryModel historyModel,
				     AccountingModel       accModel,
				     ExecutionTimingModel  execModel,
				     long timeNow,
				     long horizon,
				     long dtq,
				     int  nSeq) {
	this.candGen       = candGen;
	this.historyModel  = historyModel;
	this.accModel      = accModel;
	this.execModel     = execModel;
	this.timeNow = timeNow;
	this.horizon = horizon;
	this.dtq     = dtq;
	this.nSeq    = nSeq;

	// These are used to project hist and accounting info forward temporarily.
	eHistoryModel = new EnhancedExecutionHistoryModel(historyModel);
	eAccModel     = new EnhancedAccountingModel(accModel);

	timedLists = new Vector();

	Logger slogger = LogManager.getLogger("SIM");
	logger = new LogProxy("QLAS:Despatcher", "", slogger);

    }

   /** Return the metrics of the <i>best</i> group to execute at
     * the specified time under the specified environmental conditions.
     * @param time The time we want the selection for.
     * @param env  A snapshot of the observing environment at time.
    */
    public Metric getScheduleItem(long time, EnvironmentSnapshot env) throws Exception {

	logger.method("getscheditem").log(1, "get item at: "+ScheduleSimulator.sdf.format(new Date(time)));

	currEnv = env;

	// recalculate if sequence is used up
	if (currSequence == null || currSequence.isEmpty())
	    recalculate(time);

	// recalculate if weve overrun horizon
	if (time > currHorizonEnd)
	    recalculate(time);

	// recalculate if env has changed
	if (env.seeing < currEnv.seeing)
	     recalculate(time);
	
	// emtpy sequence after forward lookahead
	if (currSequence.isEmpty())
	    return null;
	//new Metric(null, 0.0);

	// remove and return head of sequence
	Metric m = (Metric)currSequence.get(0);
	ExecutionStatistics h = historyModel.getExecutionStatistics(m.group);
	if (execModel.canDo(m.group, time, currEnv, h)) {
	    currSequence.remove(0);
	    return m;
	} else {
	    logger.method("getscheditem").log(1, "Unable to execute planned item");
	}
	return null;

    }

    /** Calculate the next sequence to follow (upto next horizon).*/
    private void recalculate(long time) throws Exception {
	iseq++;	
	timeNow = time; 
	currHorizonEnd = timeNow + horizon;
	logger.method("recalculate").log(1, "Start re-sequencing for "+
					 ScheduleSimulator.sdf.format(new Date(timeNow))+" to "+
					 ScheduleSimulator.sdf.format(new Date(currHorizonEnd)));
	
	timedLists.clear();
	
	// work out a series of candidate lists 

	int istep = 0;
	long t = timeNow;

	// list n contains candidates for: [now + N*dtq] +dtq interval 
	while (t < currHorizonEnd) {
	 
	    List candList = candGen.getCandidateMetrics(t, currEnv);
	    timedLists.add(candList);

	    logger.log(2, "Computed lookahead candidate list for timestep: "+istep+" contains "+candList.size()+" groups");
	    t += dtq;
	    istep++;
	}
	
	int    bsn = -1;
	List   bestSeq = null;
	double bestScore = -99999.9;
	
	// series of sequence tests
	for (int i = 0; i < nSeq; i++) {
	
	    logger.log(2, "Starting sequence calculation "+i+" of "+nSeq);

	    List   seq = new Vector();
	    double seqScore = 0.0;
	    long   sumXt = 0L;
	    long   ftime = timeNow;
	    
	    // clear out back to original history and accounts (upto timeNow).	  
	    eHistoryModel.clearTemporaryHistory();
	    eAccModel.resetTemporaryAccounts();
	    logger.log(2, "Cleared temp hist and accounting");

	    // only upto horizon
	    while (sumXt < horizon) {
		
		ftime = timeNow + sumXt;
		logger.log(2, "Checking candidates at: "+ScheduleSimulator.sdf.format(new Date(ftime))+"...");

		// choose correct clist at [now + sumXt]
		int nl = (int)((double)sumXt/(double)dtq);
		logger.log(2, "Accessing timedlist "+nl);

		List candList = (List)timedLists.get(nl);

		Metric selected = selectAny(candList, ftime);
		if (selected != null) {
		    
		    long xt = execModel.getExecTime(selected.group);
		    sumXt += xt;
		    
		    seqScore += selected.score;
		    seq.add(selected);
		    logger.log(2, "Adding "+selected.group.getName()+" to built sequence");

		    // update the historymodel with temporary exec info for group selected, i.e.
		    // it looks like we "would have" executed this group.
		    
		    eHistoryModel.updateTemporaryHistory(selected.group, ftime+xt-10000L);
		    // that 10 secs is to avoid an equality test failing in the XTM this needs fixing really.
 
		    double xhours = 0.0;
		    selected.accounts.getAccount(AccountingModel.ACCOUNT_TOTAL).debit(xhours);
		    
		} else {
		    sumXt += 1*60*1000L; // jump forward to find something to do
		    logger.log(2, "Nothing available for this slot");
		}

	    }

	    if (seqScore > bestScore) {
		bsn     = i;
		bestSeq = seq;
		bestScore =seqScore;
	    }
	    
	    logger.log(2, "Sequence calculation "+i+" had "+seq.size()+" groups and total score: "+seqScore);
	    
	} // next seq

	currSequence = bestSeq;		    

	logger.log(2, "Best sequence is: "+bsn+" with score: "+bestScore);
	long tt = timeNow;
	int ii = 0;
	Iterator ib = bestSeq.iterator();
	while (ib.hasNext()) {
	    Metric m = (Metric)ib.next();
	    long gxt = execModel.getExecTime(m.group);
	    System.err.println("BESTSEQ ["+iseq+"] "+ii+"  "+ScheduleSimulator.sdf.format(new Date(tt))+" "+m.group+" until "+
			       ScheduleSimulator.sdf.format(new Date(tt+gxt)));
	    //eHistoryModel.showExecutionStatistics(m.group);
	    tt += gxt;
	    ii++;
	}
    }

    /** Select any metric from the supplied list of candidates.*/
    private Metric selectAny(List candidates, long t) {
	// cand list length L.
	// call get(n) : n in [0, L-1]
	
	if (candidates.size() == 0)
	    return null;
	//System.err.println("Select any at "+ScheduleSimulator.sdf.format(new Date(t))+" from "+candidates.size()+" candidates");
	Metric m = null;
	int ntry = 0;
	while (m == null && ntry < 10) {
	    int in = (int)Math.floor(Math.random()*(double)candidates.size());
	    m = (Metric)candidates.get(in);	

	    // keep trying till we get one thats executable
	    ExecutionStatistics h = eHistoryModel.getExecutionStatistics(m.group);
	    //System.err.println("Testing "+m.group.getName()+" with last tmp exec: "+ScheduleSimulator.sdf.format(new Date(h.lastExecuted)));
	    if (! execModel.canDo(m.group, t, currEnv, h)) {
		//System.err.println("Reject non-executable "+m.group.getName());	
		m = null;
	    }
	    ntry++;
	}
	return m; // could be null

    }

}

  
